package com.fzk.core;

import com.alibaba.fastjson2.JSON;
import com.fzk.env.conf.DataConf;
import com.fzk.log.Logger;
import com.fzk.utils.MyFileUtil;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.util.ReferenceCountUtil;
import lombok.AllArgsConstructor;
import lombok.Data;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Set;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * meta文件：存储所有文件元信息，顺序写，1GB
 * todo 1GB固定文件，超过时新建文件
 *
 * @author fzk
 * @datetime 2023-03-17 16:41:40
 */
public class FileMetaStore implements AutoCloseable {
    private final FileChannel fileChannel;// 文件元信息存储文件通道
    private final MappedByteBuffer fileMap;// 通道映射
    public volatile long writeIndex;// 写索引
    private final DataConf dataConf;
    private final FileCheckPoint checkPoint;
    private final ReadWriteLock lock = new ReentrantReadWriteLock();

    public FileMetaStore(DataConf dataConf) throws IOException {
        this.dataConf = dataConf;

        // 1.初始化checkpoint
        this.checkPoint = new FileCheckPoint(Path.of(dataConf.getDataDir(), dataConf.getCheckPoint()));

        // 2.初始化元信息存储文件
        Path path = Path.of(dataConf.getDataDir(), dataConf.getMetaFileName());
        MyFileUtil.ensureDirsAndFile(path);// 确保文件已创建
        fileChannel = FileChannel.open(path, Set.of(StandardOpenOption.WRITE, StandardOpenOption.READ));
        // 3.初始化writeIndex
        writeIndex = this.checkPoint.getMetaCheckPoint();
        // 4.映射直接内存
        fileMap = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, dataConf.getMetaFileSize());
        Logger.info(String.format("元信息储存文件 %s 初始化成功, writeIndex = %d", path.getFileName(), writeIndex));

    }

    /**
     * 写入文件元信息到末尾，并返回其offset
     *
     * @param meta 文件元信息
     * @return 该元信息存储的起始offset
     */
    public FileMetaWrapper writeMeta(FileMeta meta) throws IOException {
        lock.writeLock().lock();
        try {
            long offset = writeIndex;
            // 1.序列化元信息
            byte[] bytes = JSON.toJSONString(meta).getBytes(StandardCharsets.UTF_8);
            int len = bytes.length;// 元信息长度，不包含自身
            ByteBuf buf = PooledByteBufAllocator.DEFAULT.directBuffer(len + 4, len + 4);
            try {
                buf.writeInt(len).writeBytes(bytes);
                // 2.写入元信息储存文件
                writeIndex = doWrite(writeIndex, buf);
                // 3.刷新checkpoint
                checkPoint.updateMetaCheckPoint(writeIndex);
                Logger.info(String.format("文件 %s 成功写入元信息储存文件: %s, offset: %d, writeIndex: %d", meta.getFilename(), dataConf.getMetaFileName(), offset, writeIndex));
                return new FileMetaWrapper(offset, len, meta);
            } catch (IOException e) {
                Logger.error(String.format("文件 %s 写入元信息储存文件: %s, 出错, writeIndex: %d, exception: %s", meta.getFilename(), dataConf.getMetaFileName(), writeIndex, e));
                e.printStackTrace();
                throw e;
            } finally {
                ReferenceCountUtil.release(buf);// 必须及时释放资源
            }
        } finally {
            lock.writeLock().unlock();
        }
    }

    /**
     * 更新元信息
     * 主要用于分片上传最后分片上传成功时调整元信息文件中存储的md5值
     * 警告：必须保证meta修改前后长度是不变的，否则会造成严重后果
     *
     * @param offset    元信息存储位移
     * @param meta      必须保证meta修改前后字节数一致，否则会造成严重后果
     * @param metaBytes 元信息字节数组
     */
    public void updateMeta(long offset, FileMeta meta, byte[] metaBytes) {
        lock.writeLock().lock();
        try {
            if (offset + metaBytes.length + 4 > writeIndex) {
                Logger.error(String.format("更新元信息失败，给出的(offset: %d + metaByteLen: %d) 大于 writeIndex: %d, meta: %s", offset, metaBytes.length, writeIndex, meta));
                return;
            }
            // 判断元信息修改前后是否长度一致
            int len = fileMap.getInt((int) offset);// 元信息长度，不包含自身
            if (len != metaBytes.length) {
                Logger.error(String.format("更新元信息失败，前后字节数不一致，offset: %d, oldMetaLen: %d, newMetaLen: %d", offset, len, metaBytes.length));
                return;
            }
            ByteBuf buf = PooledByteBufAllocator.DEFAULT.directBuffer(4 + len, 4 + len);
            try {
                buf.writeInt(len).writeBytes(metaBytes);
                doWrite(offset, buf);
            } catch (Exception e) {
                Logger.error(String.format("文件 %s 更新元信息时出错, offset: %d, meta: %s, exception: %s", meta.getFilename(), offset, meta, e));
                e.printStackTrace();
                throw new RuntimeException(e);
            } finally {
                ReferenceCountUtil.release(buf);// 必须及时释放资源
            }
        } finally {
            lock.writeLock().unlock();
        }
    }

    /**
     * 将数据写入内存映射缓冲区
     * TODO 落盘策略问题: 同步(累计脏页再统一提交)? 异步?
     *
     * @param position 写入位置
     * @param buf      写入数据
     * @return 返回写入后的指针, position+ buf.readableBytes()
     */
    private long doWrite(long position, ByteBuf buf) {
        MappedByteBuffer slice = fileMap.slice();// 必须切片以保证指针独立
        slice.position((int) position);
        for (ByteBuffer buffer : buf.nioBuffers()) {
            position += buffer.remaining();
            slice.put(buffer);
        }
        slice.force();// 同步落盘
        return position;
    }

    /**
     * 从指定偏移量读取文件元信息
     *
     * @param offset 偏移量
     * @return 文件元信息，不会返回null
     */
    public FileMetaWrapper readMeta(long offset) {
        lock.readLock().lock();
        try {
            // 1.检查
            if (offset >= writeIndex) {
                String err = String.format("从%s读取文件元信息出错，offset=%d 大于此时 writeIndex=%d", dataConf.getMetaFileName(), offset, writeIndex);
                Logger.error(err);
                throw new RuntimeException(err);
            }
            // 2.读取内容
            // 2.1 读取元信息长度
            int len = fileMap.getInt((int) offset);
            if (len <= 0) {
                Logger.error(String.format("读取元信息储存文件offset=%d 的长度非法为%d", offset, len));
                throw new RuntimeException(String.format("读取元信息储存文件offset=%d 的长度非法为%d", offset, len));
            }
            // 2.2 读取元信息
            byte[] dest = new byte[len];
            fileMap.get((int) offset + 4, dest, 0, len);
            String jsonStr = new String(dest, StandardCharsets.UTF_8);

            // 3.反序列化
            FileMeta meta = JSON.parseObject(jsonStr, FileMeta.class);
            return new FileMetaWrapper(offset, len, meta);
        } finally {
            lock.readLock().unlock();
        }
    }

    @AllArgsConstructor
    @Data
    public static class FileMetaWrapper {
        public long offset;
        public int metaLen;
        public FileMeta meta;
    }

    @Override
    public void close() throws IOException {
        this.fileChannel.close();
        this.checkPoint.close();
    }
}
